option(BUILD_HACK "True if we should build the Hack typechecker." ON)

include(CMakeParseArguments)

if (NOT BUILD_HACK)
  message(STATUS "Skipping hack")
  return()
endif()

message(STATUS "Building hack")

find_package(LZ4)
find_package(LibElf)

# native_libraries: values for `-l` flags
# lib_paths: values for `-L` flags (directories)
# extra_link_opts: opaque options passed to the linker
#
# We need extra_link_opts for:
# - static libraries
# - anything built from third-party: cmake gives us the link flags
unset(extra_include_paths)
unset(extra_native_libraries)
unset(extra_lib_paths)
unset(extra_link_opts)
unset(extra_cc_flags)

# Allows '#include "hphp/path/to/library/"' paths to start from hphp
# project directory  which is consistent with fbmake's include paths.
list(APPEND extra_include_paths ${HPHP_HOME})
list(APPEND extra_cc_flags -pthread)

# Xcode/Ninja generators undefined MAKE
if(NOT MAKE)
  set(MAKE make)
endif()

if ("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}")
  set(DUNE_BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/_build")
  set(OPAM_STAMP_FILE "_build/opam.stamp")
  set(RUST_FFI_BUILD_ROOT "${DUNE_BUILD_DIR}/rust_ffi")
  set(CARGO_HOME "${DUNE_BUILD_DIR}/cargo_home")
else()
  set(DUNE_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
  set(RUST_FFI_BUILD_ROOT "${CMAKE_BINARY_DIR}")
  set(CARGO_HOME "${CMAKE_CURRENT_BINARY_DIR}/cargo_home")
  set(OPAM_STAMP_FILE "opam.stamp")
endif()
set(HACK_BUILD_ROOT "${DUNE_BUILD_DIR}/default")

get_target_property(OPAM_EXECUTABLE opam IMPORTED_LOCATION)

add_custom_command(
  OUTPUT "${OPAM_STAMP_FILE}"
  DEPENDS opam opam_setup.sh
  COMMAND
    ${CMAKE_CURRENT_SOURCE_DIR}/opam_setup.sh
    "${OPAM_EXECUTABLE}"
    "${DUNE_BUILD_DIR}"
    && cmake -E touch "${OPAM_STAMP_FILE}"
)
add_custom_target(opam_setup DEPENDS "${OPAM_STAMP_FILE}" opam_setup.sh)

if (SKIP_OPAM)
  set(OPAMROOT "~/.opam")
else ()
  set(OPAMROOT "${DUNE_BUILD_DIR}/opam")
endif()

if(LZ4_FOUND)
  list(APPEND extra_include_paths ${LZ4_INCLUDE_DIR})
  get_filename_component(pth ${LZ4_LIBRARY} DIRECTORY)
  list(APPEND extra_lib_paths ${pth})
  list(APPEND extra_native_libraries "lz4")
else()
  get_target_property(LZ4_INCLUDE_DIRS lz4 INTERFACE_INCLUDE_DIRECTORIES)
  list(APPEND extra_include_paths ${LZ4_INCLUDE_DIRS})
  # If LZ4_FOUND is false either we didn't find lz4 or we found it but it's the
  # wrong version.  We can't just add the new path and a native_lib because we
  # can't control the order (and -l won't accept the raw path to the lib).  By
  # doing it this way we specify the path explicitly.
  get_target_property(LZ4_LIBS lz4 INTERFACE_LINK_LIBRARIES)
  list(APPEND extra_link_opts ${LZ4_LIBS})
endif()

get_target_property(ZSTD_INCLUDE_DIRS zstd INTERFACE_INCLUDE_DIRECTORIES)
list(APPEND extra_include_paths ${ZSTD_INCLUDE_DIRS})
get_target_property(ZSTD_LIBS zstd INTERFACE_LINK_LIBRARIES)
list(APPEND extra_link_opts ${ZSTD_LIBS})

list(APPEND extra_include_paths ${LIBSQLITE3_INCLUDE_DIR})
get_filename_component(pth ${LIBSQLITE3_LIBRARY} DIRECTORY)
list(APPEND extra_lib_paths ${pth})
list(APPEND extra_native_libraries "sqlite3")

get_target_property(RUSTC_EXE rustc LOCATION)
get_target_property(CARGO_EXE cargo LOCATION)
get_filename_component(RUSTC_BIN_DIR "${RUSTC_EXE}" DIRECTORY)
get_filename_component(CARGO_BIN_DIR "${CARGO_EXE}" DIRECTORY)

function(invoke_dune name target)
  add_custom_target(
      ${name}
      COMMAND
        . "${CMAKE_CURRENT_BINARY_DIR}/dev_env.sh" &&
        opam exec --
        $(MAKE) --makefile=Makefile.dune ${target}
        BYTECODE="${EMIT_OCAML_BYTECODE}"
      WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    )

  add_dependencies(${name} rustc cargo zstd)
  if (NOT SKIP_OPAM)
    add_dependencies(${name} opam_setup)
  endif()
endfunction()

invoke_dune(hack_dune_debug debug)
invoke_dune(hack_dune_test test)
invoke_dune(hack_dune all)

set(CARGO_BUILD "${CMAKE_SOURCE_DIR}/hphp/hack/scripts/build_rust_to_ocaml.sh")

set(OPCODES "${CMAKE_BINARY_DIR}/hphp/tools/opcodes.rs")

add_executable(gen-rust1 "${CMAKE_SOURCE_DIR}/hphp/tools/hhbc-gen/gen-rust.cpp")
add_custom_command(
  OUTPUT "${OPCODES}"
  COMMAND gen-rust1 > "${OPCODES}"
  COMMENT "Generating opcodes.rs"
)

if(DEFINED ENV{HACKDEBUG})
  set(PROFILE "debug")
else()
  set(PROFILE "release")
endif()

set(HHBC_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/src/hackc")
set(HHBC_AST_SRCS
  "${HHBC_PREFIX}/hhbc/hackc_unit.rs"
  "${HHBC_PREFIX}/hhbc/hhas_adata.rs"
  "${HHBC_PREFIX}/hhbc/hhas_attribute.rs"
  "${HHBC_PREFIX}/hhbc/hhas_body.rs"
  "${HHBC_PREFIX}/hhbc/hhas_class.rs"
  "${HHBC_PREFIX}/hhbc/hhas_coeffects.rs"
  "${HHBC_PREFIX}/hhbc/hhas_constant.rs"
  "${HHBC_PREFIX}/hhbc/hhas_function.rs"
  "${HHBC_PREFIX}/hhbc/hhas_method.rs"
  "${HHBC_PREFIX}/hhbc/hhas_module.rs"
  "${HHBC_PREFIX}/hhbc/hhas_param.rs"
  "${HHBC_PREFIX}/hhbc/hhas_pos.rs"
  "${HHBC_PREFIX}/hhbc/hhas_property.rs"
  "${HHBC_PREFIX}/hhbc/hhas_symbol_refs.rs"
  "${HHBC_PREFIX}/hhbc/hhas_type.rs"
  "${HHBC_PREFIX}/hhbc/hhas_type_const.rs"
  "${HHBC_PREFIX}/hhbc/hhas_typedef.rs"
  "${HHBC_PREFIX}/hhbc/hhbc_ast.rs"
  "${HHBC_PREFIX}/hhbc/hhbc_ast_ffi_cbindgen.rs"  # Not in a crate.
  "${HHBC_PREFIX}/hhbc/hhbc_id.rs"
  "${HHBC_PREFIX}/hhbc/opcodes.rs"
  "${HHBC_PREFIX}/hhbc/typed_value.rs"
)

set(NAMING_SPECIAL_NAMES_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/src/naming")
set(NAMING_SPECIAL_NAMES_SRCS
   "${NAMING_SPECIAL_NAMES_PREFIX}/naming_special_names.rs"
   "${NAMING_SPECIAL_NAMES_PREFIX}/naming_special_names_ffi_cbindgen.rs"
)

set(FFI_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/src/utils/ffi")
set(FFI_SRCS
   "${FFI_PREFIX}/ffi.rs"
   "${FFI_PREFIX}/ffi_ffi_cbindgen.rs"
)

set(HHBC_AST_HEADER "${RUST_FFI_BUILD_ROOT}/hphp/hack/src/hackc/hhbc-ast.h")
set(FFI_HEADER "${RUST_FFI_BUILD_ROOT}/hphp/hack/src/utils/ffi.h")
set(NAMING_SPECIAL_NAMES_HEADER "${RUST_FFI_BUILD_ROOT}/hphp/hack/src/naming/naming-special-names.h")
set(TYPE_CONSTRAINT_HEADER "${CMAKE_SOURCE_DIR}/hphp/runtime/vm/type-constraint-flags.h")
set(FCALL_HEADER "${CMAKE_SOURCE_DIR}/hphp/runtime/vm/fcall-args-flags.h")
set(HHBC_HEADER "${CMAKE_SOURCE_DIR}/hphp/runtime/vm/hhbc-shared.h")
set(ATTR_HEADER "${CMAKE_SOURCE_DIR}/hphp/runtime/base/attr.h")

add_custom_command(
  OUTPUT ${HHBC_AST_HEADER}
  COMMAND
   . "${CMAKE_CURRENT_BINARY_DIR}/dev_env_rust_only.sh" &&
    ${CARGO_BUILD} ffi_cbindgen ffi_cbindgen --exe
      --header "${FFI_HEADER}" --namespaces "HPHP,hackc"
      ${FFI_SRCS} &&
    ${CARGO_BUILD} ffi_cbindgen ffi_cbindgen --exe
      --header "${NAMING_SPECIAL_NAMES_HEADER}" --namespaces "HPHP,hackc,hhbc"
      ${NAMING_SPECIAL_NAMES_SRCS} &&
    ${CARGO_BUILD} ffi_cbindgen ffi_cbindgen --exe
      --header "${HHBC_AST_HEADER}" --namespaces "HPHP,hackc,hhbc"
      --includes "${FFI_HEADER},${NAMING_SPECIAL_NAMES_HEADER},${TYPE_CONSTRAINT_HEADER},${ATTR_HEADER},${FCALL_HEADER},${HHBC_HEADER}"
      ${HHBC_AST_SRCS}
  DEPENDS rustc cargo "${OPCODES}"
  COMMENT "Generating hhbc-ast.h"
)

add_custom_target(
  "hhbc_ast_cbindgen"
  DEPENDS ${HHBC_AST_HEADER}
)

add_library("hhbc_ast_header" INTERFACE)
add_dependencies("hhbc_ast_header" "hhbc_ast_cbindgen")

add_custom_target(hack_rust_ffi_bridge_targets)

# Compiling cxx entrypoints for hhvm
#
# Usage:
#     build_cxx_bridge(
#       name
#       DIR directory
#       [EXTRA_SRCS src [src ...]]
#       [LINK_LIBS lib [lib ...]]
#     )
#
# Where:
#   `name` is the target name of the cxx_bridge.
#   `directory` is the required directory of the cxx_bridge sources.
#   `src` are extra source files to include in the bridge.
#   `lib` are extra link libraries to include in the bridge.
#
function(build_cxx_bridge NAME)
  cmake_parse_arguments(CXX_BRIDGE "" "DIR" "EXTRA_SRCS;LINK_LIBS" ${ARGN})

  if ("${CXX_BRIDGE_DIR}" STREQUAL "")
    message(FATAL_ERROR "Missing DIR parameter")
  endif()
  if (NOT "${CXX_BRIDGE_UNPARSED_ARGUMENTS}" STREQUAL "")
    message(FATAL_ERROR "Unexpected parameters: ${CXX_BRIDGE_UNPARSED_ARGUMENTS}")
  endif()

  set(FFI_BRIDGE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${CXX_BRIDGE_DIR}")
  set(FFI_BRIDGE_BIN "${RUST_FFI_BUILD_ROOT}/hphp/hack/${CXX_BRIDGE_DIR}")

  set(RUST_PART_LIB "${FFI_BRIDGE_BIN}/${PROFILE}/${CMAKE_STATIC_LIBRARY_PREFIX}${NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}")
  set(RUST_PART_CXX "${FFI_BRIDGE_BIN}/${NAME}.cpp")
  set(RUST_PART_HEADER "${FFI_BRIDGE_BIN}/${NAME}.rs")
  set(GENERATED "${FFI_BRIDGE_BIN}/cxxbridge/${NAME}/${NAME}")
  set(GENERATED_CXXBRIDGE "${FFI_BRIDGE_BIN}/cxxbridge")

  add_custom_command(
      OUTPUT ${RUST_PART_CXX}
      COMMAND
        ${CMAKE_COMMAND} -E make_directory "${FFI_BRIDGE_BIN}" &&
        . "${CMAKE_CURRENT_BINARY_DIR}/dev_env_rust_only.sh" &&
        ${CARGO_BUILD} "${NAME}" "${NAME}" --cxx "${FFI_BRIDGE_BIN}" &&
        ${CMAKE_COMMAND} -E copy "${GENERATED}.rs.cc" "${RUST_PART_CXX}" &&
        ${CMAKE_COMMAND} -E copy "${GENERATED}.rs.h" "${RUST_PART_HEADER}"
      WORKING_DIRECTORY ${FFI_BRIDGE_SRC}
      DEPENDS rustc cargo "${OPCODES}"
  )
  add_custom_target(
    "${NAME}_cxx"
    DEPENDS ${RUST_PART_CXX}
  )

  add_library("${NAME}" STATIC ${RUST_PART_CXX} ${CXX_BRIDGE_EXTRA_SRCS} )
  add_dependencies("${NAME}" rustc cargo "${NAME}_cxx")
  add_dependencies(hack_rust_ffi_bridge_targets "${NAME}")
  target_link_libraries("${NAME}" PUBLIC ${RUST_PART_LIB} ${CXX_BRIDGE_LINK_LIBS})
  # `-iquote` is like `-I` (or target_include_directories()`), except:
  # - it takes precedence over `-I`
  # - it only applies to `#include "foo"`, not `#include <foo>`
  target_compile_options("${NAME}" INTERFACE "-iquote" "${RUST_FFI_BUILD_ROOT}")
  target_compile_options("${NAME}" PRIVATE "-iquote" "${GENERATED_CXXBRIDGE}")
endfunction()

build_cxx_bridge(parser_ffi DIR "src/parser/ffi_bridge")
build_cxx_bridge(compiler_ffi DIR "src/hackc/ffi_bridge" LINK_LIBS hdf)
build_cxx_bridge(
  hdf
  DIR "src/utils/hdf"
  EXTRA_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/src/utils/hdf/hdf-wrap.cpp"
  LINK_LIBS folly
)
build_cxx_bridge(
  hhvm_types_ffi
  DIR "src/hackc/hhvm_cxx/hhvm_types"
  EXTRA_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/src/hackc/hhvm_cxx/hhvm_types/as-base-ffi.cpp"
)
build_cxx_bridge(
 hhvm_hhbc_defs_ffi
 DIR "src/hackc/hhvm_cxx/hhvm_hhbc_defs"
 EXTRA_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/src/hackc/hhvm_cxx/hhvm_hhbc_defs/as-hhbc-ffi.cpp"
)

if (NOT LZ4_FOUND)
  add_dependencies(hack_dune lz4)
  add_dependencies(hack_dune_debug lz4)
  add_dependencies(hack_dune_test lz4)
endif()

# Intentionally not using `hack_dune_debug` as it generates output files of a
# different format (bytecode instead of raw executables, which is useful if
# you're working with Hack, but means that e.g. hhvm can't find
# `hh_single_compile` in the source tree. Keep it around, but require it to be
# explicitly used
add_custom_target(hack ALL DEPENDS hack_dune "${OPCODES}")
add_custom_target(hack_test DEPENDS hack_dune_test "${OPCODES}")

configure_file(dev_env.sh.in dev_env.sh ESCAPE_QUOTES @ONLY)
configure_file(dev_env_common.sh.in dev_env_common.sh ESCAPE_QUOTES @ONLY)
configure_file(dev_env_rust_only.sh.in dev_env_rust_only.sh ESCAPE_QUOTES @ONLY)

install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bin/hh_client
  DESTINATION bin
  COMPONENT dev)

install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bin/hh_server
  DESTINATION bin
  COMPONENT dev)

install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bin/hackfmt
  DESTINATION bin
  COMPONENT dev)

install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bin/hh_parse
  DESTINATION bin
  COMPONENT dev)
